package io.vertx.guides.wiki

import io.vertx.core.AbstractVerticle
import io.vertx.core.Future
import io.vertx.core.eventbus.Message
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject
import io.vertx.core.logging.LoggerFactory
import io.vertx.ext.jdbc.JDBCClient
import java.io.FileInputStream
import java.io.InputStream
import java.util.*
import java.util.stream.Collectors


/**
 * Created by ashye on 2017/7/18.
 */
class WikiDatabaseVerticle : AbstractVerticle() {

    lateinit var dbClient: JDBCClient

    companion object {
        val CONFIG_WIKIDB_JDBC_URL = "wikidb.jdbc.url"
        val CONFIG_WIKIDB_JDBC_DRIVER_CLASS = "wikidb.jdbc.driver_class"
        val CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE = "wikidb.jdbc.max_pool_size"
        val CONFIG_WIKIDB_SQL_QUERIES_RESOURCE_FILE = "wikidb.sqlqueries.resource.file"

        val CONFIG_WIKIDB_QUEUE = "wikidb.queue"

        val LOGGER = LoggerFactory.getLogger(MainVerticle::class.java)
    }

    enum class SqlQuery {
        CREATE_PAGES_TABLE,
        ALL_PAGES,
        GET_PAGE,
        CREATE_PAGE,
        SAVE_PAGE,
        DELETE_PAGE
    }

    val sqlQueries = mutableMapOf<SqlQuery, String>()

    private fun loadSqlQueries() {
        val queriesFile: String? = config().getString(CONFIG_WIKIDB_SQL_QUERIES_RESOURCE_FILE)
        val inputStream: InputStream
        if (queriesFile != null) {
            inputStream = FileInputStream(queriesFile)
        }else {
            inputStream = javaClass.getResourceAsStream("/db-queries.properties")
        }

        val queriesProps = Properties()
        queriesProps.load(inputStream)
        inputStream.close()

        sqlQueries.put(SqlQuery.CREATE_PAGES_TABLE, queriesProps.getProperty("create-pages-table"))
        sqlQueries.put(SqlQuery.ALL_PAGES, queriesProps.getProperty("all-pages"))
        sqlQueries.put(SqlQuery.GET_PAGE, queriesProps.getProperty("get-page"))
        sqlQueries.put(SqlQuery.CREATE_PAGE, queriesProps.getProperty("create-page"))
        sqlQueries.put(SqlQuery.SAVE_PAGE, queriesProps.getProperty("save-page"))
        sqlQueries.put(SqlQuery.DELETE_PAGE, queriesProps.getProperty("delete-page"))
    }

    override fun start(startFuture: Future<Void>) {
        loadSqlQueries()

        dbClient = JDBCClient.createShared(vertx, JsonObject()
                .put("url", config().getString(CONFIG_WIKIDB_JDBC_URL, "jdbc:hsqldb:file:db/wiki"))
                .put("driver_class", config().getString(CONFIG_WIKIDB_JDBC_DRIVER_CLASS, "org.hsqldb.jdbcDriver"))
                .put("max_pool_size", config().getInteger(CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE, 30)));

        dbClient.getConnection { ar ->
            run {
                if (ar.failed()) {
                    LOGGER.error("Could not open a database connection", ar.cause())
                    startFuture.fail(ar.cause())
                } else {
                    val connection = ar.result()
                    connection.execute(sqlQueries.get(SqlQuery.CREATE_PAGES_TABLE)) { create ->
                        connection.close()
                        if (create.failed()) {
                            LOGGER.error("Database preparation error", create.cause())
                            startFuture.fail(create.cause())
                        } else {
                            vertx.eventBus().consumer(config().getString(CONFIG_WIKIDB_QUEUE, "wikidb.queue"), this::onMessage)
                            startFuture.complete()
                        }
                    }
                }
            }
        }
    }


    enum class ErrorCodes {
        NO_ACTION_SPECIFIED,
        BAD_ACTION,
        DB_ERROR
    }

    fun onMessage(message: Message<JsonObject>) {
        if (!message.headers().contains("action")) {
            message.fail(ErrorCodes.NO_ACTION_SPECIFIED.ordinal, "No action header specified")
        }

        val action = message.headers().get("action")

        when (action) {
            "all-pages" -> fetchAllPages(message)
            "get-page" -> fetchPage(message)
            "create-page" -> createPage(message)
            "save-page" -> savePage(message)
            "delete-page" -> deletePage(message)
            else ->
                    message.fail(ErrorCodes.BAD_ACTION.ordinal, "Bad action: $action")
        }
    }

    private fun fetchAllPages(message: Message<JsonObject>) {

        dbClient.getConnection { car ->
            if (car.succeeded()) {
                val connection = car.result()
                connection.query(sqlQueries[SqlQuery.ALL_PAGES]) { res ->
                    connection.close()
                    if (res.succeeded()) {
                        val pages = res.result()
                                .results
                                .stream()
                                .map { json -> json.getString(0) }
                                .sorted()
                                .collect(Collectors.toList())
                        message.reply(JsonObject().put("pages", JsonArray(pages)))
                    } else {
                        reportQueryError(message, res.cause())
                    }
                }
            } else {
                reportQueryError(message, car.cause())
            }
        }
    }

    private fun fetchPage(message: Message<JsonObject>) {

        val requestedPage = message.body().getString("page")

        dbClient.getConnection { car ->
            if (car.succeeded()) {
                val connection = car.result()
                connection.queryWithParams(sqlQueries[SqlQuery.GET_PAGE], JsonArray().add(requestedPage)) { fetch ->
                    connection.close()
                    if (fetch.succeeded()) {
                        val response = JsonObject()
                        val resultSet = fetch.result()
                        if (resultSet.numRows == 0) {
                            response.put("found", false)
                        } else {
                            response.put("found", true)
                            val row = resultSet.results[0]
                            response.put("id", row.getInteger(0))
                            response.put("rawContent", row.getString(1))
                        }
                        message.reply(response)
                    } else {
                        reportQueryError(message, fetch.cause())
                    }
                }
            } else {
                reportQueryError(message, car.cause())
            }
        }

    }

    private fun createPage(message: Message<JsonObject>) {
        val request = message.body()

        dbClient.getConnection { car ->

            if (car.succeeded()) {
                val connection = car.result()
                val data = JsonArray()
                        .add(request.getString("title"))
                        .add(request.getString("markdown"))

                connection.updateWithParams(sqlQueries[SqlQuery.CREATE_PAGE], data) { res ->
                    connection.close()
                    if (res.succeeded()) {
                        message.reply("ok")
                    } else {
                        reportQueryError(message, res.cause())
                    }
                }
            } else {
                reportQueryError(message, car.cause())
            }
        }
    }

    private fun savePage(message: Message<JsonObject>) {
        val request = message.body()

        dbClient.getConnection { car ->

            if (car.succeeded()) {
                val connection = car.result()
                val data = JsonArray()
                        .add(request.getString("markdown"))
                        .add(request.getString("id"))

                connection.updateWithParams(sqlQueries[SqlQuery.SAVE_PAGE], data) { res ->
                    connection.close()
                    if (res.succeeded()) {
                        message.reply("ok")
                    } else {
                        reportQueryError(message, res.cause())
                    }
                }
            } else {
                reportQueryError(message, car.cause())
            }
        }
    }

    private fun deletePage(message: Message<JsonObject>) {
        dbClient.getConnection { car ->
            if (car.succeeded()) {
                val connection = car.result()
                val data = JsonArray().add(message.body().getString("id"))
                connection.updateWithParams(sqlQueries[SqlQuery.DELETE_PAGE], data) { res ->
                    connection.close()
                    if (res.succeeded()) {
                        message.reply("ok")
                    } else {
                        reportQueryError(message, res.cause())
                    }
                }
            } else {
                reportQueryError(message, car.cause())
            }
        }
    }

    private fun reportQueryError(message: Message<JsonObject>, cause: Throwable) {
        LOGGER.error("Database query error", cause)
        message.fail(ErrorCodes.DB_ERROR.ordinal, cause.message)
    }
}

